<?php

namespace Gnway\Ticket\Rest\Util;

use Doctrine\Common\Annotations\AnnotationReader;
use Gnway\Ticket\Rest\Container\RestContainer;
use Gnway\Ticket\Rest\Exceptions\ClassNoDefException;
use ReflectionException, RuntimeException, Closure, stdClass;

final class ClassUtils
{

  /**
   * @var array
   */
  private static array $classFileMap = [];

  /**
   * Don't let anyone instantiate this class
   */
  private function __construct() {}

  /**
   * Get class file
   *
   * @param string $class
   * @return string
   */
  public static function getClassFile(string $class): string
  {
    if (!class_exists($class)) {
      throw new ClassNoDefException('Class name was empty or not defined');
    }

    if (!isset(self::$classFileMap[$class])) {
      try {
        $rf = RestContainer::getClassReflector($class);
        $file = $rf->getFileName();
        self::$classFileMap[$class] = $file ?: '';
      } catch (ReflectionException $e) {
        throw new RuntimeException($e->getMessage());
      }
    }

    return self::$classFileMap[$class];
  }

  /**
   * Get class last modification
   *
   * @param string $class
   * @return int
   */
  public static function getLastModification(string $class): int
  {
    return filemtime(self::getClassFile($class));
  }

  /**
   * Get Class Constant
   *
   * @param string $class
   * @param string $const
   * @return false|mixed|null
   * @throws ReflectionException
   */
  public static function getClassConstant(string $class, string $const)
  {
    if (!class_exists($class)) {
      throw new ClassNoDefException('Class name was empty or not defined');
    }
    if ('' === $const) {
      return NULL;
    }

    $rf = RestContainer::getClassReflector($class);
    if ( ! $rf->hasConstant($const)) {
      return NULL;
    }

    return $rf->getConstant($const);
  }

  /**
   * Parser Annotation
   *
   * @param string $class
   * @param string $annotation
   * @return object|null
   * @throws ReflectionException
   */
  public static function getAnnotation(string $class, string $annotation): ?object
  {
    if (!class_exists($class)) {
      throw new ClassNoDefException('Class name was empty or not defined');
    }
    if (!class_exists($annotation)) {
      throw new ClassNoDefException('Annotation name was empty or not defined');
    }

    return RestContainer::make(AnnotationReader::class)
                        ->getClassAnnotation(RestContainer::getClassReflector($class), $annotation);
  }

  /**
   * Get declared field values
   *
   * @param object $object
   * @return array
   * @throws ReflectionException
   */
  public static function getAllDeclaredFieldValues(object $object): array
  {
    if ($object instanceof Closure) {
      return [];
    }
    if ($object instanceof stdClass) {
      return (array)$object;
    }

    $data = [];
    $class = get_class($object);
    $properties = RestContainer::getClassReflector($class)->getProperties();

    if (empty($properties)) {
      return [];
    }

    foreach ($properties as $property) {
      $property->setAccessible(TRUE);
      $value = $property->getValue($object);
      if (is_object($value)) {
        $data[$property->getName()] = self::getAllDeclaredFieldValues($value);
      } else {
        $data[$property->getName()] = $value;
      }
    }

    return $data;
  }

  /**
   * Get declared field values
   *
   * @param object $object
   * @param array  $fields
   * @return array
   */
  public static function getDeclaredFieldValues(object $object, array $fields): array
  {
    if (sizeof($fields) < 1) {
      return [];
    }

    $result = [];
    $getter = function (string $field) {
      return $this->$field;
    };

    foreach ($fields as $k => $field) {
      $v = NULL;
      if (property_exists($object, $field)) {
        $v = $getter->call($object, $field);
      }
      $result[is_numeric($k) ? $field : $k] = $v;
    }

    return $result;
  }

  /**
   * Set value for declared field
   *
   * @param object $object
   * @param string $property
   * @param mixed  $value
   */
  public static function setDeclaredFieldValue(object $object, string $property, $value): void
  {
    if (empty($property) || !property_exists($object, $property)) {
      return;
    }

    $setter = function (string $property, $value): void {
      $this->$property = $value;
    };
    $setter->call($object, $property, $value);
  }
}